package bsh;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.HashMap;
import java.util.Map;

/**
 * 
 */
public final class PrimitiveUtils implements ParserConstants {

    private static final Map<Class, Class> PRIMITIVE_WRAPPERS = new HashMap<Class, Class>();
    static {
        PRIMITIVE_WRAPPERS.put(Boolean.TYPE, Boolean.class);
        PRIMITIVE_WRAPPERS.put(Byte.TYPE, Byte.class);
        PRIMITIVE_WRAPPERS.put(Short.TYPE, Short.class);
        PRIMITIVE_WRAPPERS.put(Character.TYPE, Character.class);
        PRIMITIVE_WRAPPERS.put(Integer.TYPE, Integer.class);
        PRIMITIVE_WRAPPERS.put(Long.TYPE, Long.class);
        PRIMITIVE_WRAPPERS.put(Float.TYPE, Float.class);
        PRIMITIVE_WRAPPERS.put(Double.TYPE, Double.class);
        PRIMITIVE_WRAPPERS.put(Boolean.class, Boolean.TYPE);
        PRIMITIVE_WRAPPERS.put(Byte.class, Byte.TYPE);
        PRIMITIVE_WRAPPERS.put(Short.class, Short.TYPE);
        PRIMITIVE_WRAPPERS.put(Character.class, Character.TYPE);
        PRIMITIVE_WRAPPERS.put(Integer.class, Integer.TYPE);
        PRIMITIVE_WRAPPERS.put(Long.class, Long.TYPE);
        PRIMITIVE_WRAPPERS.put(Float.class, Float.TYPE);
        PRIMITIVE_WRAPPERS.put(Double.class, Double.TYPE);
        PRIMITIVE_WRAPPERS.put(BigDecimal.class, BigDecimal.class);
    }

    private static final Map<Class<?>, Object> DEFAULT_VALUES = new HashMap<Class<?>, Object>();
    static {
        DEFAULT_VALUES.put(boolean.class, Boolean.FALSE);
        DEFAULT_VALUES.put(byte.class, Byte.valueOf((byte) 0));
        DEFAULT_VALUES.put(short.class, Short.valueOf((byte) 0));
        DEFAULT_VALUES.put(char.class, Character.valueOf((char) 0));
        DEFAULT_VALUES.put(int.class, Integer.valueOf((byte) 0));
        DEFAULT_VALUES.put(long.class, Long.valueOf((byte) 0));
        DEFAULT_VALUES.put(float.class, Float.valueOf((byte) 0));
        DEFAULT_VALUES.put(double.class, Double.valueOf((byte) 0));
        DEFAULT_VALUES.put(BigDecimal.class, BigDecimal.ZERO);
    }

    /**
     * 
     */
    private PrimitiveUtils() {
    }

    /**
     * @param type Class
     * @return Object
     */
    public static Object getDefaultValue(Class<?> type) {
        return DEFAULT_VALUES.get(type);
    }

    /**
     * Perform a binary operation on two Primitives or wrapper types. If both original args were Primitives return a Primitive result else
     * it was mixed (wrapper/primitive) return the wrapper type. The exception is for boolean operations where we will return the primitive
     * type either way.
     * 
     * @param obj1 Object
     * @param obj2 Object
     * @param kind int
     * @return Object
     * @param mathContext o {@link MathContext}
     */
    public static Object binaryOperation(Object obj1, Object obj2, int kind, MathContext mathContext) {
        Tuple2 operands = promotePrimitives(obj1, obj2, mathContext);
        return binaryOperationImpl(operands.left, operands.right, kind, mathContext);
    }

    /**
     * @author takeshi
     */
    private static final class Tuple2 {
        private final Object left, right;

        /**
         * @param left Obbject
         * @param right Object
         */
        private Tuple2(Object left, Object right) {
            super();
            this.left = left;
            this.right = right;
        }

    }

    /**
     * @param lhs Object
     * @param rhs Object
     * @param kind int
     * @param mathContext o math context
     * @return Object
     */
    private static Object binaryOperationImpl(Object lhs, Object rhs, int kind, MathContext mathContext) {
        if (lhs instanceof Boolean && rhs instanceof Boolean) {
            return booleanBinaryOperation((Boolean) lhs, (Boolean) rhs, kind);
        } else if (lhs instanceof Integer && rhs instanceof Integer) {
            return intBinaryOperation((Integer) lhs, (Integer) rhs, kind);
        } else if (lhs instanceof Long && rhs instanceof Long) {
            return longBinaryOperation((Long) lhs, (Long) rhs, kind);
        } else if (lhs instanceof Float && rhs instanceof Float) {
            return floatBinaryOperation((Float) lhs, (Float) rhs, kind);
        } else if (lhs instanceof Double && rhs instanceof Double) {
            return doubleBinaryOperation((Double) lhs, (Double) rhs, kind);
        } else if (lhs instanceof BigDecimal && rhs instanceof BigDecimal) {
            return bigDecimalBinaryOperation((BigDecimal) lhs, (BigDecimal) rhs, kind, mathContext);
        } else if (lhs instanceof Comparable && rhs instanceof Comparable) {
            return comparableOperation(lhs, rhs, kind);
        } else {
            return binaryObjectOperation(lhs, rhs, kind);
        }
    }

    /**
     * @param left Object
     * @param right Object
     * @param kind int
     * @return Object
     */
    private static Object binaryObjectOperation(Object left, Object right, int kind) {
        if (kind == BSHBinaryExpression.EQ) {
            if (left != null) {
                return left.equals(right);
            } else {
                return right == null;
            }
        } else if (kind == BSHBinaryExpression.NE) {
            if (left != null) {
                return !left.equals(right);
            } else {
                return right != null;
            }
        } else if (kind == BSHBinaryExpression.PLUS) {
            return String.valueOf(left) + String.valueOf(right);
        }
        throw new IllegalArgumentException("Illegal object operation: " + ParserConstants.tokenImage[kind]);
    }

    /**
     * @param lhs Object
     * @param rhs Object
     * @param kind int
     * @return Object
     */
    @SuppressWarnings("unchecked")
    private static Object comparableOperation(Object lhs, Object rhs, int kind) {
        if (lhs == null) {
            throw new NullPointerException("cannot invoke compareTo on null objects");
        }
        switch (kind) {
        case LT:
        case LTX:
            return ((Comparable<Object>) lhs).compareTo(rhs) < 0;

        case GT:
        case GTX:
            return ((Comparable<Object>) lhs).compareTo(rhs) > 0;

        case EQ:
            return ((Comparable<Object>) lhs).compareTo(rhs) == 0;

        case LE:
        case LEX:
            return ((Comparable<Object>) lhs).compareTo(rhs) <= 0;

        case GE:
        case GEX:
            return ((Comparable<Object>) lhs).compareTo(rhs) >= 0;

        case NE:
            return ((Comparable<Object>) lhs).compareTo(rhs) != 0;

        case PLUS:
            return String.valueOf(lhs) + String.valueOf(rhs);
        default:
            throw new IllegalArgumentException("Illegal Comparable operation: " + ParserConstants.tokenImage[kind]);
        }
    }

    /**
     * @param lhs Object
     * @param rhs Object
     * @param kind int
     * @param mathContext o math context
     * @return Object
     */
    private static Object bigDecimalBinaryOperation(BigDecimal lhs, BigDecimal rhs, int kind, MathContext mathContext) {
        switch (kind) {
        case LT:
        case LTX:
            return lhs.compareTo(rhs) < 0;

        case GT:
        case GTX:
            return lhs.compareTo(rhs) > 0;

        case EQ:
            return lhs.compareTo(rhs) == 0;

        case LE:
        case LEX:
            return lhs.compareTo(rhs) <= 0;

        case GE:
        case GEX:
            return lhs.compareTo(rhs) >= 0;

        case NE:
            return lhs.compareTo(rhs) != 0;

        case PLUS:
            return lhs.add(rhs, mathContext);

        case MINUS:
            return lhs.subtract(rhs, mathContext);

        case STAR:
            return lhs.multiply(rhs, mathContext);

        case SLASH:
            return lhs.divide(rhs, mathContext);

        case MOD:
            return lhs.remainder(rhs, mathContext);

        case LSHIFT:
        case LSHIFTX:
        case RSIGNEDSHIFT:
        case RSIGNEDSHIFTX:
        case RUNSIGNEDSHIFT:
        case RUNSIGNEDSHIFTX:
            throw new IllegalArgumentException("BigDecimal's cannot be shifted");

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for BigDecimal operands");
        }
    }

    /**
     * @param b1 Boolean
     * @param b2 Boolean
     * @param kind int
     * @return Boolean
     */
    private static Boolean booleanBinaryOperation(Boolean b1, Boolean b2, int kind) {
        switch (kind) {
        case EQ:
            return b1.equals(b2);

        case NE:
            return !b1.equals(b2);

        case BOOL_OR:
        case BOOL_ORX:
            return b1.booleanValue() || b2.booleanValue();

        case BOOL_AND:
        case BOOL_ANDX:
            return b1.booleanValue() && b2.booleanValue();

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for boolean operands");
        }
    }

    /**
     * @param l1 long
     * @param l2 long
     * @param kind int
     * @return Object
     */
    private static Object longBinaryOperation(Long l1, Long l2, int kind) {
        long lhs = l1.longValue();
        long rhs = l2.longValue();

        switch (kind) {
        case LT:
        case LTX:
            return lhs < rhs;

        case GT:
        case GTX:
            return lhs > rhs;

        case EQ:
            return lhs == rhs;

        case LE:
        case LEX:
            return lhs <= rhs;

        case GE:
        case GEX:
            return lhs >= rhs;

        case NE:
            return lhs != rhs;

        case PLUS:
            return lhs + rhs;

        case MINUS:
            return lhs - rhs;

        case STAR:
            return lhs * rhs;

        case SLASH:
            return lhs / rhs;

        case MOD:
            return lhs % rhs;

        case LSHIFT:
        case LSHIFTX:
            return lhs << rhs;

        case RSIGNEDSHIFT:
        case RSIGNEDSHIFTX:
            return lhs >> rhs;

        case RUNSIGNEDSHIFT:
        case RUNSIGNEDSHIFTX:
            return lhs >>> rhs;

        case BIT_AND:
        case BIT_ANDX:
            return lhs & rhs;

        case BIT_OR:
        case BIT_ORX:
            return lhs | rhs;

        case XOR:
            return lhs ^ rhs;

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for long operands");
        }
    }

    /**
     * @param i1 Integer
     * @param i2 Integer
     * @param kind int
     * @return Object
     */
    private static Object intBinaryOperation(Integer i1, Integer i2, int kind) {
        int lhs = i1.intValue();
        int rhs = i2.intValue();

        switch (kind) {
        // boolean
        case LT:
        case LTX:
            return lhs < rhs;

        case GT:
        case GTX:
            return lhs > rhs;

        case EQ:
            return lhs == rhs;

        case LE:
        case LEX:
            return lhs <= rhs;

        case GE:
        case GEX:
            return lhs >= rhs;

        case NE:
            return lhs != rhs;

            // arithmetic
        case PLUS:
            return lhs + rhs;

        case MINUS:
            return lhs - rhs;

        case STAR:
            return lhs * rhs;

        case SLASH:
            return lhs / rhs;

        case MOD:
            return lhs % rhs;

            // bitwise
        case LSHIFT:
        case LSHIFTX:
            return lhs << rhs;

        case RSIGNEDSHIFT:
        case RSIGNEDSHIFTX:
            return lhs >> rhs;

        case RUNSIGNEDSHIFT:
        case RUNSIGNEDSHIFTX:
            return lhs >>> rhs;

        case BIT_AND:
        case BIT_ANDX:
            return lhs & rhs;

        case BIT_OR:
        case BIT_ORX:
            return lhs | rhs;

        case XOR:
            return lhs ^ rhs;

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for integer operands");
        }
    }

    /**
     * @param d1 double
     * @param d2 double
     * @param kind int
     * @return Object
     */
    private static Object doubleBinaryOperation(Double d1, Double d2, int kind) {
        double lhs = d1.doubleValue();
        double rhs = d2.doubleValue();

        switch (kind) {
        // boolean
        case LT:
        case LTX:
            return lhs < rhs;

        case GT:
        case GTX:
            return lhs > rhs;

        case EQ:
            return lhs == rhs;

        case LE:
        case LEX:
            return lhs <= rhs;

        case GE:
        case GEX:
            return lhs >= rhs;

        case NE:
            return lhs != rhs;

            // arithmetic
        case PLUS:
            return lhs + rhs;

        case MINUS:
            return lhs - rhs;

        case STAR:
            return lhs * rhs;

        case SLASH:
            return lhs / rhs;

        case MOD:
            return lhs % rhs;

            // can't shift floating-point values
        case LSHIFT:
        case LSHIFTX:
        case RSIGNEDSHIFT:
        case RSIGNEDSHIFTX:
        case RUNSIGNEDSHIFT:
        case RUNSIGNEDSHIFTX:
            throw new IllegalArgumentException("Double's cannot be shifted");

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for double operands");
        }
    }

    /**
     * @param f1 float
     * @param f2 float
     * @param kind int
     * @return Object
     */
    static Object floatBinaryOperation(Float f1, Float f2, int kind) {
        float lhs = f1.floatValue();
        float rhs = f2.floatValue();

        switch (kind) {
        case LT:
        case LTX:
            return lhs < rhs;

        case GT:
        case GTX:
            return lhs > rhs;

        case EQ:
            return lhs == rhs;

        case LE:
        case LEX:
            return lhs <= rhs;

        case GE:
        case GEX:
            return lhs >= rhs;

        case NE:
            return lhs != rhs;

        case PLUS:
            return lhs + rhs;

        case MINUS:
            return lhs - rhs;

        case STAR:
            return lhs * rhs;

        case SLASH:
            return lhs / rhs;

        case MOD:
            return lhs % rhs;

        case LSHIFT:
        case LSHIFTX:
        case RSIGNEDSHIFT:
        case RSIGNEDSHIFTX:
        case RUNSIGNEDSHIFT:
        case RUNSIGNEDSHIFTX:
            throw new IllegalArgumentException("Float's cannot be shifted");

        default:
            throw new IllegalArgumentException("Illegal binary operation: " + ParserConstants.tokenImage[kind] + " for float operands");
        }
    }

    /**
     * Promote primitive wrapper type to to Integer wrapper type
     * 
     * @param wrapper Object
     * @return Object
     */
    public static Object promoteToInteger(Object wrapper) {
        if (wrapper instanceof Character) {
            return new Integer(((Character) wrapper).charValue());
        } else if ((wrapper instanceof Byte) || (wrapper instanceof Short)) {
            return new Integer(((Number) wrapper).intValue());
        }

        return wrapper;
    }

    /**
     * Checks if either of the objects are of the given type
     * 
     * @param left one of the objects
     * @param right another
     * @param type the type to check
     * @return boolean
     */
    private static boolean eitherIsOfType(Object left, Object right, Class<?> type) {
        return type.isInstance(left) || type.isInstance(right);
    }

    /**
     * Promote the pair of primitives to the maximum type of the two. e.g. [int,long]->[long,long]
     * 
     * @param lhs1 Object
     * @param rhs1 Object
     * @param mathContext the {@link MathContext}
     * @return Tuple
     */
    private static Tuple2 promotePrimitives(Object lhs1, Object rhs1, MathContext mathContext) {
        Object lhs = promoteToInteger(lhs1);
        Object rhs = promoteToInteger(rhs1);
        if ((lhs instanceof Number) && (rhs instanceof Number)) {
            Number lnum = (Number) lhs;
            Number rnum = (Number) rhs;
            if (eitherIsOfType(lnum, rnum, BigDecimal.class)) {
                lnum = promoteToBigDecimal(lnum, mathContext);
                rnum = promoteToBigDecimal(rnum, mathContext);
            } else if (eitherIsOfType(lnum, rnum, Double.class)) {
                lnum = promoteToDouble(lnum);
                rnum = promoteToDouble(rnum);
            } else if (eitherIsOfType(lnum, rnum, Float.class)) {
                lnum = promoteToFloat(lnum);
                rnum = promoteToFloat(rnum);
            } else if (eitherIsOfType(lnum, rnum, Long.class)) {
                lnum = promoteToLong(lnum);
                rnum = promoteToLong(rnum);
            }
            return new Tuple2(lnum, rnum);
        }
        return new Tuple2(lhs, rhs);
    }

    /**
     * Converts a Number to Long if necessary
     * 
     * @param num the {@link Number}
     * @return converted
     */
    private static Number promoteToLong(Number num) {
        if (num instanceof Long) {
            return num;
        }
        return num.longValue();
    }

    /**
     * Converts a Number to Float if necessary
     * 
     * @param num the {@link Number}
     * @return converted
     */
    private static Number promoteToFloat(Number num) {
        if (num instanceof Float) {
            return num;
        }
        return num.floatValue();
    }

    /**
     * Converts a Number to Double if necessary
     * 
     * @param num the {@link Number}
     * @return converted
     */
    private static Number promoteToDouble(Number num) {
        if (num instanceof Double) {
            return num;
        }
        return num.doubleValue();
    }

    /**
     * Converts a Number to a BigDecimal if necessary
     * 
     * @param num the {@link Number}
     * @param mathContext the {@link MathContext}
     * @return converted
     */
    private static Number promoteToBigDecimal(Number num, MathContext mathContext) {
        if (num instanceof BigDecimal) {
            return num;
        }
        return new BigDecimal(String.valueOf(num), mathContext);
    }

    /**
     * @param val Object
     * @param kind int
     * @param math {@link MathContext}
     * @return Object
     */
    public static Object unaryOperation(Object val, int kind, MathContext math) {
        Object operand = promoteToInteger(val);
        if (operand instanceof Boolean) {
            return booleanUnaryOperation((Boolean) operand, kind);
        } else if (operand instanceof Integer) {
            return intUnaryOperation((Integer) operand, kind);
        } else if (operand instanceof BigDecimal) {
            return bigDecimalUnaryOperation((BigDecimal) operand, kind, math);
        } else if (operand instanceof Long) {
            return longUnaryOperation((Long) operand, kind);
        } else if (operand instanceof Float) {
            return floatUnaryOperation((Float) operand, kind);
        } else if (operand instanceof Double) {
            return doubleUnaryOperation((Double) operand, kind);
        } else {
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for value: " + val);
        }
    }

    /**
     * @param operand {@link BigDecimal}
     * @param kind int
     * @param math MathContext
     * @return {@link BigDecimal}
     */
    private static BigDecimal bigDecimalUnaryOperation(BigDecimal operand, int kind, MathContext math) {
        switch (kind) {
        case PLUS:
            return operand;
        case MINUS:
            return operand.negate(math);
        case INCR:
            return operand.add(BigDecimal.ONE, math);

        case DECR:
            return operand.subtract(BigDecimal.ONE, math);

        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for BigDecimal operands");
        }
    }

    /**
     * @param b boolean
     * @param kind int
     * @return boolean
     */
    static boolean booleanUnaryOperation(Boolean b, int kind) {
        boolean operand = b.booleanValue();
        switch (kind) {
        case BANG:
            return !operand;
        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for boolean operand");
        }
    }

    /**
     * @param i Integer
     * @param kind int
     * @return int
     */
    static int intUnaryOperation(Integer i, int kind) {
        int operand = i.intValue();

        switch (kind) {
        case PLUS:
            return operand;
        case MINUS:
            return -operand;
        case TILDE:
            return ~operand;
        case INCR:
            return operand + 1;
        case DECR:
            return operand - 1;
        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for integral operand");
        }
    }

    /**
     * @param l long
     * @param kind int
     * @return long
     */
    static long longUnaryOperation(Long l, int kind) {
        long operand = l.longValue();

        switch (kind) {
        case PLUS:
            return operand;
        case MINUS:
            return -operand;
        case TILDE:
            return ~operand;
        case INCR:
            return operand + 1;
        case DECR:
            return operand - 1;
        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for long operand");
        }
    }

    /**
     * @param f float
     * @param kind int
     * @return float
     */
    static float floatUnaryOperation(Float f, int kind) {
        float operand = f.floatValue();

        switch (kind) {
        case PLUS:
            return operand;
        case MINUS:
            return -operand;
        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for long operand");
        }
    }

    /**
     * @param d double
     * @param kind int
     * @return double
     */
    static double doubleUnaryOperation(Double d, int kind) {
        double operand = d.doubleValue();

        switch (kind) {
        case PLUS:
            return operand;
        case MINUS:
            return -operand;
        default:
            throw new IllegalArgumentException("Illegal unary operation: " + ParserConstants.tokenImage[kind] + " for double operand");
        }
    }

    /**
     * @param type Class
     * @return boolean
     */
    public static boolean isWrapperType(Class type) {
        return type != null && (PRIMITIVE_WRAPPERS.get(type) != null && !type.isPrimitive());
    }

}
